const isObject = (target: any) =>
  (typeof target === 'object' || typeof target === 'function') &&
  target !== null;

const getType = (target: any): any => Object.prototype.toString.call(target);

const getInitialValue = (obj: any) => {
  const Constructor = obj.constructor;
  return new Constructor();
};

const getInitialOtherValue = (obj: any) => {
  const Constructor = obj.constructor;

  switch (getType(obj)) {
    case '[object Number]':
    case '[object String]':
    case '[object Boolean]':
    case '[object RegExp]':
    case '[object Error]':
    case '[object Date]':
      return new Constructor(obj);
    case '[object Function]':
      return new Function('return ' + obj.toString())();
    default:
      return new Constructor();
  }
};

export function deepClone(target: any, map = new WeakMap()) {
  if (!isObject(target)) return target;

  const type: any = getType(target);
  let cloneObj = null;

  if (
    [
      '[object Object]',
      '[object Map]',
      '[Object Array]',
      '[object Set]',
    ].includes(type)
  ) {
    cloneObj = getInitialValue(target);
  } else {
    cloneObj = getInitialOtherValue(target);
  }

  if (map.get(target)) return map.get(target);

  map.set(target, cloneObj);

  if (type === '[object Object]') {
    for (const k in target) {
      cloneObj[k] = deepClone(target[k], map);
    }
  } else if (type === '[object Array]') {
    target.forEach((n: any, index: any) => {
      cloneObj[index] = deepClone(n, map);
    });
  } else if (type === '[object Map]') {
    for (const [key, value] of target) {
      cloneObj.set(key, deepClone(value, map));
    }
  } else if (type === '[object Set]') {
    for (const k of target) {
      cloneObj.add(deepClone(k, map));
    }
  }

  return cloneObj;
}
